package jamun

import scala.xml._
import java.io.File
import com.ctc.wstx.stax._
import  javax.xml.stream.events._
import java.sql._

case class Repo(name:String, baseURL:String)

/** A "managed" package */
case class MngdPackage(name:String,version:String,release:String, summary:String,location:String,
  provides:Seq[RpmEntry], reqs:Seq[RpmEntry], repo:Option[Repo], time:Long)

/** RpmEntry is the information found in the "format" tag of the package */
case class RpmEntry(name:String, flags:Option[String],
  epoch:Option[String], ver:Option[String], rel:Option[String], pre:Option[String])

/**
  This object is for working with "managed" packages. Here managed means "managed by jamun".
  The way I look at it is, once a package has been installed via rpm,
  it is no longer "managed by jamun". But, until then it is.
*/
object ManagedPkgs {
  private lazy val dbs = getDBs

  private lazy val filterByNameQueries:Map[Repo,PreparedStatement] = Map[Repo,PreparedStatement]() ++ (dbs map {rNc =>
    val (repo,conn:Connection) = rNc
    (repo -> (conn prepareStatement ("select name,version,release,time from PKG where name like ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)))
  })

  private lazy val getAllQueries:Map[Repo,PreparedStatement] = Map[Repo,PreparedStatement]() ++ (dbs map {rNc =>
    val (repo,conn:Connection) = rNc
    (repo -> (conn prepareStatement ("select name,version,release,time from PKG", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)))
  })

  private lazy val getProviderQueries:Map[Repo,PreparedStatement] = Map[Repo,PreparedStatement]() ++ (dbs map {rNc =>
    val (repo,conn:Connection) = rNc
    // val stmt = (conn prepareStatement ("select name,version,release,time from PKG where PKG.id in (select PROVIDES.id from PROVIDES where (PROVIDES.name = ?))", ResultSet.FETCH_FORWARD,ResultSet.TYPE_FORWARD_ONLY ))
    /* The above query takes huge amount of time unless the following index is created */
    // createStmt.execute( "create unique index PROVIDES_ID_NAME_IDX on PROVIDES (id, name)")

    val stmt = (conn prepareStatement ("select PKG.name,PKG.version,PKG.release,PKG.time from PKG,PROVIDES where (PKG.id = PROVIDES.id) and (PROVIDES.name = ?)", ResultSet.FETCH_FORWARD,ResultSet.TYPE_FORWARD_ONLY ))
    stmt.setFetchSize(1)
    (repo -> stmt)
  })

  private lazy val getRequirementsQueries:Map[Repo,PreparedStatement] = Map[Repo,PreparedStatement]() ++ (dbs map {rNc =>
    val (repo,conn:Connection) = rNc
    (repo -> (conn prepareStatement ("select REQUIRES.name from REQUIRES,PKG where (REQUIRES.id = PKG.ID) and (PKG.name = ?)", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)))
  })

  def wildCardConvert(s:String) = {
    val s1 = s replace ('*', '%')
    s1 replace ('?', '_')
  }

  private def getLatest(l:Seq[MngdPackage]) = {
    // TODO may need to take care of repository priorities in case of conflicting packages
    // get only top of tree
    if (l.length > 1) {
      val totMap = scala.collection.mutable.HashMap[String, MngdPackage]()
      l foreach {p =>
        if (totMap isDefinedAt p.name) {
          // update to latest one
          if (pkgMoreRecent(p, totMap(p.name))) {
            totMap(p.name) = p
          }
        } else {
          totMap += (p.name -> p)
        }
      }

      totMap.values.toList
    } else l.toList
  }

  def extractPkgResult(repo:Repo, resultSet:ResultSet) = {
    if (resultSet.next) {
      var pkgList:List[MngdPackage] = Nil
      var keepGoing = true
      while (keepGoing) {
        pkgList ::= MngdPackage(resultSet.getString(1), resultSet.getString(2), resultSet.getString(3), "summ", "", Nil, Nil, Some(repo), resultSet.getLong(4))

        keepGoing = resultSet.next
      }
      pkgList
    } else Nil
  }

  /* TODO: Use pkg version for matching */
  def getRequirements(pkgName:String) = {
    getRequirementsQueries flatMap {rNs =>
      val (repo, stmt) = rNs
      stmt setString (1, pkgName)
      val resultSet = stmt.executeQuery
      val reqs = if (resultSet.first) {
        var reqList:List[String] = Nil
        while (!resultSet.isAfterLast) {
          reqList ::= resultSet.getString(1)
          resultSet.next
        }
        reqList
      } else Nil
      resultSet.close
      reqs
    }

  }

  def getAll = {
    val allPkgs = getAllQueries flatMap {rNs =>
      val (repo, stmt) = rNs
      val resultSet = stmt.executeQuery
      val repoPkgs = extractPkgResult(repo, resultSet)
      resultSet.close
      repoPkgs
    }

    getLatest(allPkgs.toSeq)
  }

  def getProviders(requirement:String) = {
    val allPkgs = getProviderQueries flatMap {rNs =>
      // val start = System.currentTimeMillis
      val (repo, stmt) = rNs
      stmt setString (1, requirement)
      val resultSet = stmt.executeQuery

      // val endQuery = System.currentTimeMillis

      val repoPkgs = extractPkgResult(repo, resultSet)


      resultSet.close
      // val endExtract = System.currentTimeMillis
      // println ("Query: " + (endQuery - start) + " Total: " + (endExtract - start))
      repoPkgs
    }

    getLatest(allPkgs.toSeq)
  }

  def getFilteredByName(namePattern:String) = {
    val allPkgs = filterByNameQueries flatMap {rNs =>
      val (repo, stmt) = rNs
      stmt setString (1, wildCardConvert(namePattern))
      val resultSet = stmt.executeQuery
      val repoPkgs = extractPkgResult(repo, resultSet)
      resultSet.close
      repoPkgs
    }

    getLatest(allPkgs.toSeq)
  }

  private def pkgMoreRecent(p1:MngdPackage, p2:MngdPackage) = p1.time > p2.time

  implicit def xml2helper (node:Node) = new {
    def getChildOpt (label:String) = {
      val children = (node \ label)
      if (children.length > 0) Some(children(0))
      else None
    }
    def getChild (label:String) = (node \ label)(0)

    def getPropOpt (label:String) = getChild(label) map (o => o.text)
    def getProp (label:String) = getChild(label) text

    def getAttrib (label:String) = (node attribute label).get.apply(0) text
    def getAttribOpt (label:String) = (node attribute label).map(_.apply(0) text)
  }

  private def getDBs = {
    getRepos map (r => (r, getDB(r).get))
  }

  private def getRpmEntries(eventR:javax.xml.stream.XMLEventReader) = {
    var entries:List[RpmEntry] = Nil
    var done = false
    while (!done) {
      (eventR nextEvent) match {
        case e:StartElement => {
          val name = (e getAttributeByName "name").getValue
          if (!(entries exists (_.name == name)))
            entries ::= RpmEntry(name, None, None, None, None,None)
        }
        case e:EndElement => if ((e getName).getLocalPart != "entry") done = true
        case _ => /* Ignore */
      }
    }
    entries
  }

  private def getPkg(eventR : javax.xml.stream.XMLEventReader) = {
    val name = getString (eventR)
    ignoreElement (eventR) // arch
    val (version,release) = getVersionRelease (eventR)
    ignoreElement (eventR) // checksum
    ignoreElement (eventR) // summary
    ignoreElement (eventR) // description
    ignoreElement (eventR) // packager
    ignoreElement (eventR) // url
    val fileTime = getFileTime (eventR)
    ignoreElement (eventR) // size
    ignoreElement (eventR) // location

    var done = false
    var reqs:List[RpmEntry] = Nil
    var provides:List[RpmEntry] = Nil
    while (!done) {
      (eventR nextEvent) match {
        case e:StartElement => 
          if ((e getName).getLocalPart == "requires") {
            reqs = getRpmEntries(eventR)
          } else if ((e getName).getLocalPart == "provides") {
            provides = getRpmEntries(eventR)
          }
        case e:EndElement => if ((e getName).getLocalPart == "format") done = true
        case _ => /* Ignore */
      }
    }

    done = false
    while (!done) {
      (eventR nextEvent) match {
        case e:EndElement => if ((e getName).getLocalPart == "package") done = true
        case _ => /* Ignore */
      }
    }

    MngdPackage(name,version,release,"","", provides, reqs, None, fileTime)
  }

  implicit def s2qname(s:String) = new javax.xml.namespace.QName(s)

  private def getFileTime(eventR : javax.xml.stream.XMLEventReader) = {
    val elem = eventR.nextTag.asInstanceOf[StartElement]
    (elem getAttributeByName "file").getValue.toLong
  }
  private def getVersionRelease(eventR : javax.xml.stream.XMLEventReader) = {
    val elem = eventR.nextTag.asInstanceOf[StartElement]
    ((elem getAttributeByName "ver").getValue, (elem getAttributeByName "rel").getValue)
  }

  private def ignoreElement(eventR : javax.xml.stream.XMLEventReader) = {
    eventR nextTag // ignore start
    // consume the end event
    var done = false
    while (!done) {
      (eventR nextEvent) match {
        case e:EndElement => done = true
        case _ => /* Ignore */
      }
    }
  }

  private def getString(eventR : javax.xml.stream.XMLEventReader) = {
    eventR nextTag // ignore start
    val retStr = (eventR nextEvent) match {
      case c:Characters => c getData
    }

    // consume the end event
    var done = false
    while (!done) {
      (eventR nextEvent) match {
        case e:EndElement => done = true
        case _ => /* Ignore */
      }
    }

    retStr
  }

  private def createDB(r:Repo, repoCacheDirName:String, xmldbFile : File) = {
    val gzInStream = new java.util.zip.GZIPInputStream(new java.io.FileInputStream(xmldbFile))
    val eventReader = (new WstxInputFactory) createXMLEventReader (gzInStream)

    val createProps = new java.util.Properties()
    createProps.setProperty("create", "true")

    val conn = java.sql.DriverManager getConnection("jdbc:derby:" + repoCacheDirName + "/primary.derby", createProps)
    val createStmt = conn.createStatement
    println("Creating db for: " + r.name)
    createStmt.execute( "create table PKG (" +
                        "id integer primary key, " +
                        "name character varying (60), " +
                        "version character varying (40), " + 
                        "release character varying (40), " + 
                        "time bigint)")
    createStmt.execute( "create table REQUIRES (" +
                        "id integer, foreign key (id) references PKG(id), " +
                        "name character varying (256), " +
                        "version character varying (40))")
    createStmt.execute( "create table PROVIDES (" +
                        "id integer, foreign key (id) references PKG(id), " +
                        "name character varying (256), " +
                        "version character varying (40))")

    val insertPkgStmt = conn.prepareStatement("insert into PKG values (?, ?, ?, ?, ?)")
    val insertRequiresStmt = conn.prepareStatement("insert into REQUIRES values (?, ?, ?)")
    val insertProvidesStmt = conn.prepareStatement("insert into PROVIDES values (?, ?, ?)")
    conn.setAutoCommit(false)

    var uniqueId = 0
    while (eventReader hasNext) {

      (eventReader nextEvent) match {
        case s:StartElement => (s getName) getLocalPart match {
          case "package" => {
            val p = getPkg(eventReader)

            // insert into PKG table
            insertPkgStmt.setInt(1, uniqueId)
            insertPkgStmt.setString(2, p.name)
            insertPkgStmt.setString(3, p.version)
            insertPkgStmt.setString(4, p.release)
            insertPkgStmt.setLong(5, p.time)
            insertPkgStmt.execute

            p.reqs foreach {r =>
              // insert into REQUIRES table
              insertRequiresStmt.setInt(1, uniqueId)
              insertRequiresStmt.setString(2, r.name)
              insertRequiresStmt.setString(3, "dummy")
              insertRequiresStmt.execute
            }

            p.provides foreach {r =>
              // insert into PROVIDES table
              insertProvidesStmt.setInt(1, uniqueId)
              insertProvidesStmt.setString(2, r.name)
              insertProvidesStmt.setString(3, "dummy")
              insertProvidesStmt.execute
            }

            uniqueId += 1
          }
          case _ => /* ignore */
        }
        case _ => /* ignore */
      }
    }
    createStmt.execute( "create index PROVIDES_NAME_IDX on PROVIDES (name)")
    createStmt.execute( "create index REQUIRES_ID_IDX on REQUIRES (id)")
    createStmt.close
    conn.commit
    conn
  }

  private def openDB(r:Repo, repoCacheDirName:String, xmldbFile : File) = {
    // check if database exists
    val dbFile = new File (repoCacheDirName + "/primary.derby")
    if (dbFile.exists) {
      println (r.name + ": Opening existing DB")
      java.sql.DriverManager getConnection("jdbc:derby:"+repoCacheDirName + "/primary.derby")
    }
    else
      createDB(r, repoCacheDirName, xmldbFile)
  }

  private def getDB(r:Repo):Option[Connection] = {
    val cacheDirName = Main.config("cacheDir")
    val cacheDir = new File(cacheDirName)
    if (cacheDir.isDirectory && cacheDir.canRead) {
      val repoCacheDirName = cacheDirName + "/" + r.name
      val repoCacheDir = new File(repoCacheDirName)
      val dirExists = if (!repoCacheDir.exists)
                        repoCacheDir.mkdir
                      else true

      if (!dirExists) {
        println ("[" + r.name + "] Couldn't use/create directory: " + repoCacheDirName)
        None
      } else {
        // Check if primary.xml.gz exists
        val downloadNeeded =  updateDbNeeded(r.baseURL, repoCacheDirName)

        // Figure out the primary path
        def getPrimaryPath(node:Node) = {
          val primary = ((node \\ "data") filter (d => (d getAttrib "type") == "primary")) (0)
          println(primary \"location")
          (primary \ "location")(0) getAttrib("href")
        }
        val latestRepoMdName = repoCacheDirName + "/repomd.xml"
        val primaryPath = getPrimaryPath(XML.loadFile(latestRepoMdName))

        val repoCacheName = repoCacheDirName + "/primary.xml.gz"
        val xmldbFile = new File(repoCacheName)

        if (downloadNeeded || (! xmldbFile.exists)) {
          def downloadDB = Downloader.get(r.baseURL + "/" + primaryPath, repoCacheName)

          if (downloadDB == false) {
            println("["+r.name+"] Couldn't download the package database")
            None
          } else {
            Some(createDB(r, repoCacheDirName, xmldbFile))
          }
        } else {
          Some(openDB(r, repoCacheDirName, xmldbFile))
        }
      }
    } else {
      println ("["+r.name+"] Couldn't use " + cacheDirName + " as cacheDir")
      None
    }
  }

  /** Figures out if an a new "primary.xml.gz" needs to be downloaded */
  private def updateDbNeeded(baseUrl:String, repoDirName:String) = {
    def getTimeStamp(node:Node) = {
      val primary = ((node \\ "data") filter (d => (d getAttrib "type") == "primary")) (0)
      (primary getProp "timestamp") toLong
    }

    if (Main.config("autoCheckUpdates") == "true") {
      // obsessive compulsion to get the repomd :-(
      // TODO: Perhaps we can have a timer here to prevent too frequent fetches
      val latestRepoMdName = repoDirName + "/repomd.xml"
      val latestRepoMd = new File(latestRepoMdName)

      // time for a change
      val prevRepoMd = new File(repoDirName + "/prev_repomd.xml")
      if (prevRepoMd.exists) {
        prevRepoMd.delete
      }
      if (latestRepoMd.exists) {
        latestRepoMd.renameTo(prevRepoMd)
      }
      Downloader.get(baseUrl + "/repodata/repomd.xml", latestRepoMdName)

      if (prevRepoMd.exists) {
        val latestTimeStamp = getTimeStamp(XML.loadFile(latestRepoMd))
        val prevTimeStamp = getTimeStamp(XML.loadFile(prevRepoMd))

        // we need a refresh if
        (latestTimeStamp > prevTimeStamp)
      } else true

    } else {
      false
    }
  }

  private def getRepos = {
    // get repos directory
    val repoDirName = Main.config("repoDir")
    val repoDir = new File(repoDirName)

    if (repoDir.isDirectory && repoDir.canRead) {
      val repoConfigFiles = repoDir.listFiles filter (_.isDirectory == false)

      (repoConfigFiles flatMap (f => getRepoFile(repoDirName + "/" + f.getName))) toList
    } else {
      println ("Couldn't use " + repoDirName + " as repoDir")
      Nil
    }
  }

  private def getRepoFile(configFile:String) = {
    val confRoot = XML.loadFile(configFile)
    (confRoot \\ "repository") map (getRepo(_))
  }

  private def getRepo(repoNode : Node) = {
    var baseURL = (repoNode getProp "baseURL")
    baseURL = baseURL replace ("${releaseVer}", releaseVer)
    baseURL = baseURL replace ("${baseArch}", baseArch)
    Repo(repoNode getProp "name", baseURL)
  }

  val baseArch = Main.config("baseArch")
  val releaseVer = Main.config("releaseVer")

}

